#include <rpc/auth_unix.h>
#include <rpc/xdr.h>
#include <sys/stat.h>

#include <err.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "tftp.h"


/* DEFINITIONS ****************************************************************/

#define ERR_FOPEN           "could not open file"
#define ERR_FWRITE          "could not write to file"
#define ERR_NOAUTH          "UNIX authorization needed"
#define ERR_OP_REJ          "operation rejected"
#define ERR_PERM            "permission denied"

typedef enum state_t {
  STATUS_IDLE = -1,     /* Accetta GET o PUT          */
  STATUS_GET  = 0,      /* Accetta GET_BLOCK o ERROR  */
  STATUS_PUT  = 1       /* Accetta PUT_BLOCK o ERROR  */
} state_t;

/* Credenziali utente */
typedef struct user_t {
  char* machname;
  uid_t uid;
  gid_t gid;
} user_t;

/* Informazioni su un file */
typedef struct file_t {
  FILE* file;
  char* path;
} file_t;

/******************************************************************************/


/* PROTOTYPES *****************************************************************/

/* Aggiorna la struttura `*user` con i dati di autenticazione in `*cred` */
void user_cred_cpy(user_t* user, const struct authunix_parms* cred);

/* Confronta i dati in `*user` con i dati in `*cred` */
bool_t user_cred_cmp(const user_t* user, const struct authunix_parms* cred);

/* Determina se le credenziali in `*user` permettono la lettura di `filename` */
bool_t user_canread(const user_t* user, const char* filename);

/* Determina se le credenziali in `*user` permettono la scrittura del *
 * file con percorso `filename` */
bool_t user_canwrite(const user_t* user, const char* filename);

/* Wrapper fopen(): in caso di successo, aggiorna la struttura `*file`; *
 * per comodita' restituisce una copia del puntatore a FILE risultante  */
FILE* f_open(file_t* file, const char* filename, const char* mode);

/* Wrapper fclose(): aggiorna la struttura `*file` */
void f_close(file_t* file);

/* Come f_close() con set dell'owner con le credenziali in `*user` */
void f_close_chown(file_t* file, const user_t* user);

/* La chiamata a queste funzioni fa modificare `*m` in modo da riflettere *
 * situazione di errore                                                   */
void set_maybe_t_err(maybe_t* m, errcode_t errc, const errstr_t errs);
void set_maybe_block_t_err(maybe_block_t* m, errcode_t errc, const errstr_t errs);

/******************************************************************************/


/* GLOBALS ********************************************************************/

/* Stato globale del server TFTP */
state_t g_status  = STATUS_IDLE;

/* Credenziali dell'ultimo utente che ha chiamato PUT o GET e ha        *
 * oltrepassato la fase 2 di autenticazione (stato valido e auth UNIX)  */
user_t g_user     = { NULL, -1, -1 };

/* Dati relativi al file correntemente aperto */
file_t g_file     = { NULL, NULL };

/******************************************************************************/


/**
 *  GET
**/
maybe_t* get_1_svc(req_t* req, struct svc_req* svc_req) {
  static maybe_t result;

  /* Controlli di validita' sulla richiesta ***********************************/
  if (g_status != STATUS_IDLE) {                                /* 1: STATO   */
    set_maybe_t_err(&result, ILL_OP_TFTP, ERR_OP_REJ);
    return &result;
  }
  if (svc_req->rq_cred.oa_flavor != AUTH_UNIX) {                /* 2: AUTHOR. */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_NOAUTH);
    return &result;
  }
  user_cred_cpy(&g_user, (struct authunix_parms*)svc_req->rq_clntcred);
  if (user_canread(&g_user, req->filename) == FALSE) {          /* 3: PERM.   */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_PERM);
    return &result;
  }
  /* Richiesta valida *********************************************************/
  if (f_open(&g_file, req->filename, "rb") == NULL) {           /* 4: ACCESSO */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_FOPEN);
  }
  else {
    /* Richiesta processata con successo **************************************/
    result.ok = TRUE;                                           /* 5: UPDATE  */
    g_status = STATUS_GET;
  }
  return &result;
} /* END OF: get_1_svc() */


/**
 *  PUT
**/
maybe_t* put_1_svc(req_t* req, struct svc_req* svc_req) {
  static maybe_t result;

  /* Controlli di validita' sulla richiesta ***********************************/
  if (g_status != STATUS_IDLE) {                                /* 1: STATO   */
    set_maybe_t_err(&result, ILL_OP_TFTP, ERR_OP_REJ);
    return &result;
  }
  if (svc_req->rq_cred.oa_flavor != AUTH_UNIX) {                /* 2: AUTHOR. */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_NOAUTH);
    return &result;
  }
  user_cred_cpy(&g_user, (struct authunix_parms*)svc_req->rq_clntcred);
  if (user_canwrite(&g_user, req->filename) == FALSE) {         /* 3: PERM.   */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_PERM);
    return &result;
  }
  /* Richiesta valida *********************************************************/
  if (f_open(&g_file, req->filename, "wb") == NULL) {           /* 4: ACCESSO */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_FOPEN);
  }
  else {
    /* Richiesta processata con successo **************************************/
    result.ok = TRUE;                                           /* 5: UPDATE  */
    g_status = STATUS_PUT;
  }
  return &result;
} /* END OF: put_1_svc() */


/**
 *  GET BLOCK
**/
maybe_block_t* get_block_1_svc(void* dummy, struct svc_req* svc_req) {
  static maybe_block_t result;
  static bool_t free_need = FALSE;
  char block[MAX_BLOCK_SIZE];
  int size;

  /* Free XDR del parametro di ritorno per la chiamata precedente */
  if (free_need == TRUE) {
    xdr_free((xdrproc_t)xdr_maybe_block_t, (char*)&result);
    free_need = FALSE;
  }
  /* Controlli di validita' sulla richiesta ***********************************/
  if (g_status != STATUS_GET) {                                 /* 1: STATO   */
    set_maybe_block_t_err(&result, ILL_OP_TFTP, ERR_OP_REJ);
    return &result;
  }
  if (svc_req->rq_cred.oa_flavor != AUTH_UNIX) {                /* 2: AUTHOR. */
    set_maybe_block_t_err(&result, ACCESS_VIOLATION, ERR_NOAUTH);
    return &result;
  }
                                                                /* 3: CREDEN. */
  if (user_cred_cmp(&g_user, (struct authunix_parms*)svc_req->rq_clntcred) == FALSE) {
    set_maybe_block_t_err(&result, ILL_OP_TFTP, ERR_OP_REJ);
    return &result;
  }
  /* Richiesta valida *********************************************************/
  size = fread(block, 1, MAX_BLOCK_SIZE, g_file.file);          /* 4: ACCESSO */
  result.ok = TRUE;
  result.maybe_block_t_u.block.block_t_len = size;
  result.maybe_block_t_u.block.block_t_val = malloc(size);
  if (result.maybe_block_t_u.block.block_t_val == NULL) {
    errx(EXIT_FAILURE, "malloc() failed");
  }
  memcpy(result.maybe_block_t_u.block.block_t_val, block, size);

  if (size < MAX_BLOCK_SIZE) {
    f_close(&g_file);
    g_status = STATUS_IDLE;
  }
  free_need = TRUE;
  return &result;
} /* END OF: get_block_1_svc() */


/**
 *  PUT BLOCK
**/
maybe_t* put_block_1_svc(block_t* block, struct svc_req* svc_req) {
  static maybe_t result;

  /* Controlli di validita' sulla richiesta ***********************************/
  if (g_status != STATUS_PUT) {                                 /* 1: STATO   */
    set_maybe_t_err(&result, ILL_OP_TFTP, ERR_OP_REJ);
    return &result;
  }
  if (svc_req->rq_cred.oa_flavor != AUTH_UNIX) {                /* 2: AUTHOR. */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_NOAUTH);
    return &result;
  }
                                                                /* 3: CREDEN. */
  if (user_cred_cmp(&g_user, (struct authunix_parms*)svc_req->rq_clntcred) == FALSE) {
    set_maybe_t_err(&result, ILL_OP_TFTP, ERR_OP_REJ);
    return &result;
  }
  /* Richiesta valida *********************************************************/
                                                                /* 4: ACCESSO */
  if (fwrite(block->block_t_val, 1, block->block_t_len, g_file.file) !=
                                                          block->block_t_len) {
    /* Errore: Abort dell'operazione */
    set_maybe_t_err(&result, ACCESS_VIOLATION, ERR_FWRITE);
    f_close_chown(&g_file, &g_user);
    g_status = STATUS_IDLE;
  }
  else {
    /* Richiesta processata con successo **************************************/
    result.ok = TRUE;
    if (block->block_t_len < MAX_BLOCK_SIZE) {
      /* Termine operazione: finalizzazione e passaggio allo stato IDLE *******/
      f_close_chown(&g_file, &g_user);
      g_status = STATUS_IDLE;
    }
  }
  return &result;
} /* END OF: put_block_1_svc() */


/**
 *  ERROR
**/
void* error_1_svc(err_t* err, struct svc_req* svc_req) {
  static char result;

  /* Controlli di validita' sulla richiesta ***********************************/
  if (g_status == STATUS_IDLE) {                                /* 1: STATO   */
    return &result; // ILL_OP_TFTP: ERR_OP_REJ
  }
  if (svc_req->rq_cred.oa_flavor != AUTH_UNIX) {                /* 2: AUTHOR. */
    return &result; // ACCESS_VIOLATION: ERR_NOAUTH
  }
                                                                /* 3: CREDEN. */
  if (user_cred_cmp(&g_user, (struct authunix_parms*)svc_req->rq_clntcred) == FALSE) {
    return &result; // ILL_OP_TFTP: ERR_OP_REJ
  }
  /* Richiesta valida *********************************************************/
  if (g_status == STATUS_PUT) {
    f_close_chown(&g_file, &g_user);
  }
  else {
    f_close(&g_file);
  }
  g_status = STATUS_IDLE;
  return &result;
} /* END OF: error_1_svc() */


/* IMPLEMENTATION OF LOCAL FUNCTIONS ******************************************/
/* NOTE: le funzioni locali al modulo non accedono alle variabili globali */

/**
 *  COPY USER CREDENTIALS
**/
void user_cred_cpy(user_t* user, const struct authunix_parms* cred) {
  if (user->machname != NULL) {
    free(user->machname);
  }
  user->machname = strdup(cred->aup_machname);
  user->uid = cred->aup_uid;
  user->gid = cred->aup_gid;
} /* END OF: user_cred_cpy() */


/**
 *  COMPARE USER CREDENTIALS
**/
bool_t user_cred_cmp(const user_t* user, const struct authunix_parms* cred) {
  return (  (user->uid == cred->aup_uid) &&
            (user->gid == cred->aup_gid) &&
            (strcmp(user->machname, cred->aup_machname) == 0) )? TRUE:FALSE;
} /* END OF: user_cred_cmp() */


/**
 *  CHECK FILE/DIR READ PERMISSIONS
**/
bool_t user_canread(const user_t* user, const char* filename) {
  struct stat buf;
  if (stat(filename, &buf) != 0) {
    return FALSE;
  }
  return (    ((user->uid == buf.st_uid) && ((buf.st_mode & S_IRUSR) == S_IRUSR))
           || ((user->gid == buf.st_gid) && ((buf.st_mode & S_IRGRP) == S_IRGRP))
           || ((buf.st_mode & S_IROTH) == S_IROTH) )? TRUE:FALSE;
} /* END OF: user_canread() */


/**
 *  CHECK FILE/DIR WRITE PERMISSIONS
**/
bool_t user_canwrite(const user_t* user, const char* filename) {
  struct stat buf;

  if (stat(filename, &buf) != 0) {
    /* stat del file fallito: tentativo di stat della directory */
    char* path = strdup(filename);
    char* p = strrchr(path, '/');
    if (p != NULL) {
      *p = '\0';
    }
    else {
      /* fallimento estrazione directory: si considera la directory corrente */
      free(path);
      path = strdup(".");
    }
    if (stat(path, &buf) != 0) {
      free(path);
      return FALSE;
    }
    free(path);
  }

  return (    ((user->uid == buf.st_uid) && ((buf.st_mode & S_IWUSR) == S_IWUSR))
           || ((user->gid == buf.st_gid) && ((buf.st_mode & S_IWGRP) == S_IWGRP))
           || ((buf.st_mode & S_IWOTH) == S_IWOTH) )? TRUE:FALSE;
} /* END OF: user_canwrite() */


/**
 *  OPEN FILE UPDATING STRUCT
**/
FILE* f_open(file_t* file, const char* filename, const char* mode) {
  if (file->path != NULL) {
    free(file->path);
    file->path = NULL;
  }
  file->file = fopen(filename, mode);
  if (file->file != NULL) {
    file->path = strdup(filename);
  }
  return file->file;
} /* END OF: f_open() */


/**
 * CLOSE FILE UPDATING STRUCT
**/
void f_close(file_t* file) {
  if (file->file != NULL) {
    fclose(file->file);
    file->file = NULL;
  }
  if (file->path != NULL) {
    free(file->path);
    file->path = NULL;
  }
} /* END OF: f_close() */


/**
 *  CLOSE AND CHOWN FILE UPDATING STRUCT
**/
void f_close_chown(file_t* file, const user_t* user) {
  if (file->file != NULL) {
    fclose(file->file);
    file->file = NULL;
  }
  if (file->path != NULL) {
    chown(file->path, user->uid, user->gid);
    free(file->path);
    file->path = NULL;
  }
} /* END OF: f_close_chown() */


/**
 *  SET MAYBE_T AS ERROR
**/
void set_maybe_t_err(maybe_t* m, errcode_t errc, errstr_t errs) {
  m->ok = FALSE,
  m->maybe_t_u.err.errcode = errc;
  m->maybe_t_u.err.errstr = errs;
} /* END OF: set_maybe_t_err() */


/**
 *  SET MAYBE_BLOCK_T AS ERROR
**/
void set_maybe_block_t_err(maybe_block_t* m, errcode_t errc, errstr_t errs) {
  m->ok = FALSE,
  m->maybe_block_t_u.err.errcode = errc;
  m->maybe_block_t_u.err.errstr = errs;
} /* END OF: set_maybe_block_t_err() */

